﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using SimpleSpreadsheet.Core.Expressions;
using SimpleSpreadsheet.Expressions;
using SimpleSpreadsheet.Framework;

namespace SimpleSpreadsheet.Model
{
    /// <summary>
    /// Model for a worksheet cell
    /// </summary>
    public class Cell : PropertyChangedBase
    {
        private Expression _expr;
        private Expression _originalExpression;
        private string _originalText;

        public Cell(Worksheet w, Expression expr, int column, int row)
        {
            References = new HashSet<Cell>();
            Dependents = new HashSet<Cell>();
            Worksheet = w;
            Expr = expr;
            Row = row;
            Column = column;
            Name = ReferenceRangeExpression.GetCellNameByIndexes(Row, Column);
        }

        public bool IsGrey { get; set; }
        public bool IsBlack { get; set; }
        public HashSet<Cell> References { get; set; }
        public HashSet<Cell> Dependents { get; set; }

        public string Name { get; private set; }
        public string Text { get; set; }
        public int Row { get; set; }
        public int Column { get; set; }
        public decimal Value { get; set; }
        public Worksheet Worksheet { get; private set; }

        /// <summary>
        /// Cell's expression
        /// </summary>
        public Expression Expr
        {
            get { return _expr; }
            set
            {
                if (_expr != value)
                {
                    _expr = value;
                    Evaluate();
                }
            }
        }

        /// <summary>
        /// Whether cell contains an expression
        /// </summary>
        public bool HasExpression
        {
            get { return Expr != null; }
        }

        /// <summary>
        /// Whether cell depends on other cells
        /// </summary>
        public bool HasReferences
        {
            get { return References.Count > 0; }
        }

        /// <summary>
        /// Whether other cells depend on this one
        /// </summary>
        public bool HasDependents
        {
            get { return Dependents.Count > 0; }
        }

        /// <summary>
        /// Returns value to be displayed on a worksheet
        /// </summary>
        public string DisplayValue
        {
            get
            {
                if (Expr != null)
                    return Value.ToString(CultureInfo.InvariantCulture.NumberFormat);

                return String.Empty;
            }
        }

        /// <summary>
        /// Evaluates the value in cell
        /// </summary>
        public void Evaluate()
        {
            Value = Expr != null ? EvaluateExpression() : 0;
        }

        /// <summary>
        /// Recalculates the cell and all its dependents
        /// </summary>
        public void ChainRecalculate()
        {
            Evaluate();

            if (Dependents.Count > 0)
            {
                foreach (Cell dependent in Dependents)
                {
                    dependent.ChainRecalculate();
                }
            }
        }

        /// <summary>
        /// Evaluates cell's expression
        /// </summary>
        /// <returns></returns>
        private decimal EvaluateExpression()
        {
            Expr.ReferenceValueProvider = Worksheet;
            return Convert.ToDecimal(Expr.Evaluate());
        }

        /// <summary>
        /// Check circular references
        /// </summary>
        /// <returns></returns>
        public bool Validate()
        {
            foreach (Cell cell in GetAllReferencesInDepth(Expr))
            {
                if (cell.Name == Name)
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Add referenced cell
        /// </summary>
        /// <param name="cell"></param>
        private void AddReference(Cell cell)
        {
            Dependents.Add(cell);
        }

        /// <summary>
        /// Remove referenced cell
        /// </summary>
        /// <param name="cell"></param>
        private void RemoveReference(Cell cell)
        {
            Dependents.Remove(cell);
        }

        /// <summary>
        /// Add dependent cell
        /// </summary>
        /// <param name="cell"></param>
        private void AddDependent(Cell cell)
        {
            References.Add(cell);
        }

        /// <summary>
        /// Remove dependent cell
        /// </summary>
        /// <param name="cell"></param>
        private void RemoveDependent(Cell cell)
        {
            References.Remove(cell);
        }

        /// <summary>
        /// Clears the cell and its dependencies lists
        /// </summary>
        public void Clear()
        {
            Dependents.Clear();
            References.Clear();
            Expr = null;
            Text = null;
            Value = 0;
        }

        /// <summary>
        /// Connects the cell to all its references and dependents
        /// </summary>
        public void Subscribe()
        {
            foreach (Cell cell in GetAllReferences(Expr))
            {
                cell.AddReference(this);
                AddDependent(cell);
            }
        }

        /// <summary>
        /// Disconnects the cell from all its references and dependents
        /// </summary>
        public void Unsubscribe()
        {
            foreach (Cell cell in GetAllReferences(Expr))
            {
                cell.RemoveReference(this);
                RemoveDependent(cell);
            }
        }

        /// <summary>
        /// Temporarily saves the original values when update starts
        /// </summary>
        public void BeginUpdate()
        {
            _originalText = Text;
            _originalExpression = Expr;
        }

        /// <summary>
        /// Clears the temporary values
        /// </summary>
        public void EndUpdate()
        {
            _originalText = null;
            _originalExpression = null;
        }

        /// <summary>
        /// Restores the original values if update has been cancelled
        /// </summary>
        public void CancelUpdate()
        {
            Text = _originalText;
            Expr = _originalExpression;
        }

        /// <summary>
        /// Find all cells which this one depends on (direct dependents only)
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        private HashSet<Cell> GetAllReferences(Expression expression)
        {
            var cells = new HashSet<Cell>();
            if (expression != null)
            {
                List<string> referencedCells = expression.References.ToList();
                if (referencedCells.Count != 0)
                {
                    foreach (string cellName in referencedCells)
                    {
                        cells.Add(Worksheet.GetCellByName(cellName));
                    }
                }
            }
            return cells;
        }

        /// <summary>
        /// Find all cells which this one depends on (on all depth levels)
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        private HashSet<Cell> GetAllReferencesInDepth(Expression expression)
        {
            var cells = new HashSet<Cell>();
            if (expression != null)
            {
                var expressions = new Stack<Expression>();
                expressions.Push(expression);
                do
                {
                    Expression current = expressions.Pop();
                    List<string> referencedCells = current.References.ToList();
                    if (referencedCells.Count != 0)
                    {
                        foreach (string cellName in referencedCells)
                        {
                            Cell cell = Worksheet.GetCellByName(cellName);

                            // if this cell is not yet in hashset, then its children must be visited too
                            if (cells.Add(cell))
                            {
                                if (cell.Expr != null)
                                    expressions.Push(cell.Expr);
                            }
                        }
                    }
                } while (expressions.Count > 0);
            }
            return cells;
        }

        public override string ToString()
        {
            return Name;
        }
    }
}